    - name: stage3_2_요건사실검색_문서작성
      description: 청구권별로 요건사실 검색 및 요건사실 문서 작성
      tools:
        mcpServers:
          localdocs:
            type: streamable-http
            url: "http://mcp-localdocs:8012/mcp"
            description: Get the content of local documents
          weaviate:
            type: streamable-http
            url: "https://weaviate.eroomai.com/mcp"
            description: Hybrid search on Weaviate vector DB
          code-executor:
            type: streamable-http
            url: https://code-executor.mcp.eroomai.com/mcp
            description: Run scripts of programming languages
            headers:
              Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM=

      prevs: [stage3_1_청구요건사실검색쿼리생성]
      nexts: [stage_4_청구항변반박전략]

      map_reduce:
        max_concurrency: 8

        # ── Source: queries_legal_facts_search.json의 queries 배열 ──
        source:
          mcp:
            server: localdocs
            tool_name: read_docs
            parameters: { doc_names: ["queries_legal_facts_search.json"] }
            key: ["queries"]

        # ── Shared Context: claims 배열 (reduce에서 claim_title 참조용) ──
        shared_context:
          - name: source_json
            mcp:
              server: localdocs
              tool_name: read_docs
              parameters: { doc_names: ["queries_legal_facts_search.json"] }

        # ── Map: query별 Weaviate 검색 + 결과 요약 ──
        map:
          tasks:
            # ── Step 1: Weaviate Hybrid Search (Direct MCP) ──
            - task_name: search_weaviate
              mcp: weaviate
              tool_name: search_hybrid
              parameters:
                collection_name: "{{item.search_params.collection_name}}"
                tenant: "{{item.search_params.tenant}}"
                query: "{{item.search_params.query}}"
                alpha: "{{item.search_params.alpha}}"
                limit: "{{item.search_params.limit}}"
                query_properties: "{{item.search_params.query_properties}}"
                fusion_type: "{{item.search_params.fusion_type}}"
                bm25_operator: "{{item.search_params.bm25_operator}}"
                bm25_minimum_match: "{{item.search_params.bm25_minimum_match}}"

            # ── Step 2: 검색 결과 요약 (LLM) ──
            - task_name: summarize_search
              llm_provider: anthropic
              llm_model: claude-haiku-4-5
              prompts:
                - role: user
                  content: |
                    # Task

                    아래 Weaviate 검색 결과를 분석하여, 해당 사건종류의 **요건사실(legally required facts)** 관점에서 핵심 포인트를 정리하라.

                    # 쿼리 정보
                    - query_id: {{item.query_id}}
                    - 사건종류: {{item.사건종류}}
                    - 적용 claim_ids: {{item.applicable_claim_ids}}
                    - 검색 파라미터:
                      - collection: {{item.search_params.collection_name}}
                      - tenant: {{item.search_params.tenant}}
                      - query: {{item.search_params.query}}
                      - alpha: {{item.search_params.alpha}}
                      - limit: {{item.search_params.limit}}
                      - bm25_operator: {{item.search_params.bm25_operator}}
                      - bm25_minimum_match: {{item.search_params.bm25_minimum_match}}

                    # Weaviate 검색 결과

                    {{prev.search_weaviate}}

                    # 출력 규칙 (STRICT JSON)

                    아래 JSON 형식으로만 출력하라. 마크다운 펜스, 코멘트, 설명 일체 금지.

                    {
                      "query_id": "<query_id>",
                      "사건종류": "<사건종류>",
                      "applicable_claim_ids": [<claim_ids>],
                      "search_quality": "good|partial|poor",
                      "key_points": [
                        "<요건사실 핵심 포인트 1>",
                        "<요건사실 핵심 포인트 2>",
                        "..."
                      ],
                      "hits": [
                        {
                          "weaviate_ref": "<document id or reference>",
                          "score": <float>,
                          "excerpt": "<240자 이내 발췌, 줄바꿈 제거>"
                        }
                      ],
                      "missing_elements": ["<누락된 요건사실 요소>"]
                    }

                    ## key_points 작성 규칙
                    - 3~7개의 bullet로 작성
                    - 각 포인트는 검색 hit의 excerpt에 근거하여 작성 (근거 없는 창작 금지)
                    - 요건사실(성립요건) 중심으로 정리
                    - 법률 개념어 사용, 사건 고유 사실 배제

                    ## hits 작성 규칙
                    - 최대 6개까지만 포함 (관련성 높은 순)
                    - excerpt는 240자 이내, 줄바꿈/탭 제거, 공백 정리
                    - score는 Weaviate 반환 점수 그대로 사용

                    ## search_quality 판정
                    - good: 핵심 요건사실 요소 대부분 커버 (4개 이상 key_points)
                    - partial: 일부 요건사실만 커버 (2~3개 key_points)
                    - poor: 관련 결과 거의 없음 (1개 이하 key_points)

                    ## missing_elements 작성 규칙
                    - search_quality가 "good"이 아닌 경우에만 작성
                    - 검색으로 확인되지 않은 요건사실 요소를 나열
                    - 예: ["무자력 요건", "사해의사 입증"]

        # ── Reduce: 전체 결과 조립 → MD 문서 생성 ──
        reduce:
          # ── Step 1: Python으로 MD 문서 조립 ──
          - task_name: assemble_document
            mcp: code-executor
            tool_name: run_code
            parameters:
              language: python
              timeout: 120
              network: "agent-network"
              code: |
                import json, base64
                from datetime import datetime

                # ── 1. map_results 디코딩 ──
                raw = base64.b64decode('{{map_results_b64}}').decode('utf-8')
                map_results = json.loads(raw)

                # ── 2. shared_context에서 claims 추출 ──
                source_raw = '''{{shared.source_json}}'''
                try:
                    source_data = json.loads(source_raw)
                    claims_list = source_data.get("claims", [])
                    query_summary_src = source_data.get("query_summary", {})
                    validation_warnings = source_data.get("validation_warnings", [])
                except Exception:
                    claims_list = []
                    query_summary_src = {}
                    validation_warnings = []

                # claim_id → claim_title 매핑
                claim_title_map = {c["claim_id"]: c.get("claim_title", "") for c in claims_list}

                # ── 3. map 결과 파싱 ──
                query_sections = []
                errors = []
                total_queries = len(map_results)
                all_claim_ids = set()
                case_types = set()

                for item_result in map_results:
                    idx = item_result.get("item_index", "?")
                    item = item_result.get("item", {})
                    tr = item_result.get("task_results", {})

                    query_id = item.get("query_id", f"Q-{idx}")
                    case_type = item.get("사건종류", "UNKNOWN")
                    claim_ids = item.get("applicable_claim_ids", [])
                    sp = item.get("search_params", {})

                    all_claim_ids.update(claim_ids)
                    case_types.add(case_type)

                    # summarize_search 결과 파싱
                    summary_str = tr.get("summarize_search", "")
                    summary = None
                    if summary_str and not str(summary_str).startswith("ERROR"):
                        cleaned = str(summary_str).strip()
                        if cleaned.startswith("```"):
                            lines = cleaned.split("\n")
                            lines = [l for l in lines if not l.strip().startswith("```")]
                            cleaned = "\n".join(lines)
                        try:
                            summary = json.loads(cleaned)
                        except json.JSONDecodeError as e:
                            errors.append(f"item {idx} ({query_id}) summary parse error: {e}")
                    else:
                        errors.append(f"item {idx} ({query_id}) summarize_search: {str(summary_str)[:200]}")

                    # 섹션 데이터 구성
                    query_sections.append({
                        "query_id": query_id,
                        "case_type": case_type,
                        "claim_ids": claim_ids,
                        "search_params": sp,
                        "summary": summary,
                    })

                # query_id 순서로 정렬
                query_sections.sort(key=lambda x: x["query_id"])

                # ── 4. Markdown 문서 조립 ──
                lines = []
                lines.append("# 요건사실 검색 결과 (legally_required_facts_information)")
                lines.append("")
                lines.append("## 문서 메타")
                lines.append(f"- 입력 파일: queries_legal_facts_search.json")
                lines.append(f"- 총 query 수: {total_queries}")
                lines.append(f"- 총 커버 claim 수: {len(all_claim_ids)}")
                lines.append(f"- distinct 사건종류 수: {len(case_types)}")
                lines.append(f"- 생성일시: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
                lines.append("")

                if validation_warnings:
                    lines.append("## Validation Warnings (쿼리 생성 단계)")
                    for w in validation_warnings:
                        lines.append(f"- {w}")
                    lines.append("")

                lines.append("---")
                lines.append("")

                for section in query_sections:
                    qid = section["query_id"]
                    ct = section["case_type"]
                    cids = section["claim_ids"]
                    sp = section["search_params"]
                    summary = section["summary"]

                    # 헤더
                    claim_titles = [f"{cid} ({claim_title_map.get(cid, '')})" for cid in cids]
                    lines.append(f"## {qid} — {ct}")
                    lines.append("")
                    lines.append(f"- **적용 claim_ids**: {', '.join(claim_titles)}")
                    lines.append(f"- **Target**: {sp.get('collection_name', 'N/A')} / {sp.get('tenant', 'N/A')}")
                    lines.append(f"- **검색 파라미터**: alpha={sp.get('alpha')}, limit={sp.get('limit')}, bm25_operator={sp.get('bm25_operator')}, bm25_minimum_match={sp.get('bm25_minimum_match')}, query_properties={sp.get('query_properties')}")
                    lines.append("")

                    if summary:
                        quality = summary.get("search_quality", "unknown")
                        lines.append(f"### 검색 품질: {quality}")
                        lines.append("")

                        # 요건사실 핵심 포인트
                        key_points = summary.get("key_points", [])
                        if key_points:
                            lines.append("### 요건사실(성립요건) 핵심 포인트")
                            for kp in key_points:
                                lines.append(f"- {kp}")
                            lines.append("")

                        # 근거 hit 목록
                        hits = summary.get("hits", [])
                        if hits:
                            lines.append("### 근거 Hit 목록")
                            for h in hits[:6]:
                                ref = h.get("weaviate_ref", "N/A")
                                score = h.get("score", 0)
                                excerpt = h.get("excerpt", "")[:240]
                                lines.append(f"- [{ref}] (score={score}) {excerpt}")
                            lines.append("")

                        # 검색 품질 경고
                        missing = summary.get("missing_elements", [])
                        if quality != "good" and missing:
                            lines.append("### 검색 품질 경고")
                            lines.append(f"- 검색 품질: {quality}")
                            lines.append(f"- 누락된 요건사실 요소: {', '.join(missing)}")
                            lines.append("")
                    else:
                        lines.append("### 검색 품질 경고")
                        lines.append("- 요약 결과를 파싱하지 못했습니다.")
                        lines.append("")

                    lines.append("---")
                    lines.append("")

                if errors:
                    lines.append("## 처리 오류")
                    for e in errors:
                        lines.append(f"- {e}")
                    lines.append("")

                print("\n".join(lines))

          # ── Step 2: MD 문서 저장 ──
          - task_name: save_document
            mcp: localdocs
            tool_name: write_file
            parameters:
              path: "legally_required_facts_information.md"
              content: "{{prev.assemble_document}}"